/*
 * Copyright 2014 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package org.springframework.xd.dirt.web.config;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.accept.ContentNegotiationStrategy;

/**
 * Setup Spring Security for the http endpoints of the application.
 *
 * @author Gunnar Hillert
 * @author Eric Bottard
 */
@Configuration
@ConditionalOnProperty("security.basic.enabled")
@EnableWebMvcSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

	public static final Pattern AUTHORIZATION_RULE;

	static {
		String methodsRegex = StringUtils.arrayToDelimitedString(HttpMethod.values(), "|");
		AUTHORIZATION_RULE = Pattern.compile("(" + methodsRegex + ")\\s+(.+)\\s+=>\\s+(.+)");
	}

	@Autowired
	private ContentNegotiationStrategy contentNegotiationStrategy;

	@Value("${security.basic.realm}")
	private String realm;

	@Autowired
	private AuthorizationConfig config;

	@Bean
	public AuthorizationConfig config() {
		return new AuthorizationConfig();
	}


	@Override
	protected void configure(HttpSecurity http) throws Exception {

		final RequestMatcher textHtmlMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy,
				MediaType.TEXT_HTML);

		final String loginPage = "/admin-ui/login";

		BasicAuthenticationEntryPoint basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint();
		basicAuthenticationEntryPoint.setRealmName(realm);
		basicAuthenticationEntryPoint.afterPropertiesSet();

		ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry security =
				http.csrf().disable()
						.authorizeRequests()
						.antMatchers("/admin-ui/styles/**").permitAll()
						.antMatchers("/admin-ui/images/**").permitAll()
						.antMatchers("/admin-ui/fonts/**").permitAll()
						.antMatchers("/assets/**").permitAll();

		security = configureSimpleSecurity(security);

		security
				.anyRequest().authenticated()
			.and()
				.formLogin()
				.loginPage(loginPage)
				.loginProcessingUrl("/admin-ui/login").defaultSuccessUrl("/admin-ui/")
				.permitAll()
			.and()
				.logout().logoutUrl("/admin-ui/logout")
				.permitAll()
			.and()
				.httpBasic()
			.and()
				.exceptionHandling()
				.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint(loginPage), textHtmlMatcher)
				.defaultAuthenticationEntryPointFor(basicAuthenticationEntryPoint, AnyRequestMatcher.INSTANCE);
	}

	/**
	 * Read the configuration for "simple" (that is, not ACL based) security and apply it.
	 */
	private ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
	configureSimpleSecurity(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry security) {
		for (String rule : config.getRules()) {
			Matcher matcher = AUTHORIZATION_RULE.matcher(rule);
			Assert.isTrue(matcher.matches(), String.format("Unable to parse security rule [%s], expected format is 'HTTP_METHOD ANT_PATTERN => SECURITY_ATTRIBUTE(S)'", rule));

			HttpMethod method = HttpMethod.valueOf(matcher.group(1));
			String urlPattern = matcher.group(2);
			String attribute = matcher.group(3);

			security = security.antMatchers(method, urlPattern).access(attribute);
		}
		return security;
	}

	/**
	 * Holds configuration for the authorization aspects of security.
	 *
	 * @author Eric Bottard
	 */
	@ConfigurationProperties(prefix = "xd.security.authorization")
	public static class AuthorizationConfig {
		private List<String> rules = new ArrayList<String>();

		public List<String> getRules() {
			return rules;
		}

		public void setRules(List<String> rules) {
			this.rules = rules;
		}
	}
}
